function testLog() {
    baseLog("/////////////////////////////////////////////////////////////////");
    baseLog("Hello chapter-9.js!");
}

function testDelegateBase() {
    {
        const target = {
            id: 'target'
        };
        const handler = {};
        const proxy = new Proxy(target, handler);
        baseLog(target.id);     // 'target'
        baseLog(proxy.id);      // 'target'
        target.id = 'foo';
        baseLog(target.id);     // 'foo'
        baseLog(proxy.id);      // 'foo'
        proxy.id = 'bar';
        baseLog(target.id);     // 'bar'
        baseLog(proxy.id);      // 'bar'
        baseLog(target.hasOwnProperty('id'));       // true
        baseLog(proxy.hasOwnProperty('id'));        // true
        baseLog(target === proxy);                  // false
    }
    {
        const target = {
            foo: 'bar'
        };
        const handler = {
            get(trapTarget, property, receiver) {
                /**
                 * trapTarget = target,
                 * property = 'bar',
                 * receiver = proxy,
                 */
                let decoration = '';
                if (property === 'foo') {
                    decoration = '!!!';
                }
                return Reflect.get(...arguments) + decoration;
            }
        };
        const proxy = new Proxy(target, handler);
        baseLog(target.foo);                    // 'bar'
        baseLog(proxy.foo);                     // 'bar!!!'
        baseLog(target['foo']);                 // 'bar'
        baseLog(proxy['foo']);                  // 'bar!!!'
        baseLog(Object.create(target)['foo']);  // 'bar'
        baseLog(Object.create(proxy)['foo']);   // 'bar!!!'
        const { proxy2, revoke } = Proxy.revocable(target, handler);
        // baseLog(proxy2.foo);                    // 'bar!!!', it does not work, got the TypeError, not sure why.
        revoke();
        // baseLog(proxy2.foo);                    // TypeError
    }
    {
        {
            const o = {};
            try {
                Object.defineProperty(o, 'foo', 'bar');
                baseLog('success');
            } catch (e) {
                baseLog('failure');
            }
        }
        // Below block has the same functionality as the above block.
        {
            const o = {};
            if (Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
                baseLog('success');
            } else {
                baseLog('failure');
            }
        }
    }
    {
        const target = {
            foo: 'bar'
        };
        const firstProxy = new Proxy(target, {
            get() {
                baseLog('first proxy');
                return Reflect.get(...arguments);
            }
        });
        const secondProxy = new Proxy(firstProxy, {
            get() {
                baseLog('second proxy');
                return Reflect.get(...arguments);
            }
        });
        baseLog(secondProxy.foo);       // second proxy, first proxy, bar
    }
    {
        const wm = new WeakMap();
        class User {
            constructor(userId) {
                wm.set(this, userId);
            }
            set id(userId) {
                wm.set(this, userId);
            }
            get id() {
                return wm.get(this);
            }
        }
        const user = new User(123);
        baseLog(user.id);       // 123
        const userInstanceProxy = new Proxy(user, {});
        baseLog(userInstanceProxy.id);      // undefined
        const UserClassProxy = new Proxy(User, {});
        const proxyUser = new UserClassProxy(456);
        baseLog(proxyUser.id);      // 456
    }
}

function testCatcherAndReflection() {
    {
        const target1 = {};
        const proxy1 = new Proxy(target1, {
            get(target, property, receiver) {
                baseLog('get()');
                return Reflect.get(...arguments);
            },
            set(target, property, value, receiver) {
                baseLog('set()');
                return Reflect.set(...arguments);
            },
            has(target, property) {
                baseLog('has()');
                return Reflect.has(...arguments);
            },
            defineProperty(target, property, descriptor) {
                baseLog('defineProperty()');
                return Reflect.defineProperty(...arguments);
            },
            getOwnPropertyDescriptor(target, property) {
                baseLog('getOwnPropertyDescriptor()');
                return Reflect.getOwnPropertyDescriptor(...arguments);
            },
            deleteProperty(target, property) {
                baseLog('deleteProperty()');
                return Reflect.deleteProperty(...arguments);
            },
            ownKeys(target) {
                baseLog('ownKeys()');
                return Reflect.ownKeys(...arguments);
            },
            getPrototypeOf(target) {
                baseLog('getPrototypeOf()');
                return Reflect.getPrototypeOf(...arguments);
            },
            setPrototypeOf(target, prototype) {
                baseLog('setPrototypeOf()');
                return Reflect.setPrototypeOf(...arguments);
            },
            isExtensible(target) {
                baseLog('isExtensible()');
                return Reflect.isExtensible(...arguments);
            },
            preventExtensions(target) {
                baseLog('preventExtensions()');
                return Reflect.preventExtensions(...arguments);
            }
        });
        proxy1.foo;              // 'get()'
        proxy1.foo = 'bar';      // 'set()'
        'foo' in proxy1;         // 'has()'
        Object.defineProperty(proxy1, 'foo', {value: 'bar'});    // 'defineProperty()'
        Object.getOwnPropertyDescriptor(proxy1, 'foo');          // 'getOwnPropertyDescriptor()'
        delete proxy1.foo;       // 'deleteProperty()'
        Object.keys(proxy1);     // 'ownKeys()'
        Object.getPrototypeOf(proxy1);       // 'getPrototypeOf()'
        Object.setPrototypeOf(proxy1, Object);   // 'setPrototypeOf()'
        Object.isExtensible(proxy1);         // 'isExtensible()'
        Object.preventExtensions(proxy1);    // 'preventExtensions()'

        const target2 = () => {};
        const proxy2 = new Proxy(target2, {
            apply(target, thisArg, ...argumentsList) {
                baseLog('apply()');
                return Reflect.apply(...arguments);
            }
        });
        proxy2();        // 'apply()'

        const target3 = function() {};
        const proxy3 = new Proxy(target3, {
            construct(target, argumentList, newTarget) {
                baseLog('construct()');
                return Reflect.construct(...arguments);
            }
        });
        new proxy3;      // 'construct()'
    }
}

function testProxyPattern() {
    // track visiting attributes
    {
        const user = {
            name: 'Vicky'
        };
        const proxy = new Proxy(user, {
            get(target, property, receiver) {
                baseLog(`getting ${property}`);
                return Reflect.get(...arguments);
            },
            set(target, property, value, receiver) {
                baseLog(`setting ${property} = ${value}`);
                return Reflect.set(...arguments);
            }
        });
        proxy.name;         // getting name
        proxy.age = 30;     // setting age = 30
    }
    // hide attributes
    {
        const hiddenProperties = ['foo', 'bar'];
        const targetObject = {
            foo: 1,
            bar: 2,
            baz: 3
        };
        const proxy = new Proxy(targetObject, {
            get(target, property) {
                if (hiddenProperties.includes(property)) {
                    return undefined;
                } else {
                    return Reflect.get(...arguments);
                }
            },
            has(target, property) {
                if (hiddenProperties.includes(property)) {
                    return false;
                } else {
                    return Reflect.has(...arguments);
                }
            }
        });
        // get()
        baseLog(proxy.foo);             // undefined
        baseLog(proxy.bar);             // undefined
        baseLog(proxy.baz);             // 3
        // has
        baseLog('foo' in proxy);        // false
        baseLog('bar' in proxy);        // false
        baseLog('baz' in proxy);        // true
    }
    // verify attributes
    {
        const target = {
            onlyNumbersGoHere: 0
        };
        const proxy = new Proxy(target, {
            set(target, property, value) {
                if (typeof value !== 'number') {
                    return false;
                } else {
                    return Reflect.set(...arguments);
                }
            }
        });
        proxy.onlyNumbersGoHere = 1;
        baseLog(proxy.onlyNumbersGoHere);       // 1
        proxy.onlyNumbersGoHere = '2';
        baseLog(proxy.onlyNumbersGoHere);       // 1
    }
    // verify method\s and constructor's parameters
    {
        {
            function median(...nums) {
                return nums.sort()[Math.floor(nums.length / 2)];
            }
            const proxy = new Proxy(median, {
                apply(target, thisArg, argumentList) {
                    for (const arg of argumentList) {
                        if (typeof arg !== 'number') {
                            throw 'Non-number argument provided';
                        }
                    }
                    return Reflect.apply(...arguments);
                }
            });
            baseLog(proxy(4, 7, 1));        // 4
            // baseLog(proxy(4, '7', 1));      // Error: Non-number argument provided
        }
        {
            class User {
                constructor(id) {
                    this.id_ = id;
                }
            }
            const proxy = new Proxy(User, {
                construct(target, argumentList, newTarget) {
                    if (argumentList[0] === undefined) {
                        throw 'User cannot be instantiated without id';
                    } else {
                        return Reflect.construct(...arguments);
                    }
                }
            });
            new proxy(1);
            // new proxy();                // Error: User cannot be instantiated without id
        }
    }
    // data binding and observerable objects
    {
        {
            const userList = [];
            class User {
                constructor(name) {
                    this.name_ = name;
                }
            }
            const proxy = new Proxy(User, {
                construct() {
                    const newUser = Reflect.construct(...arguments);
                    userList.push(newUser);
                    return newUser;
                }
            });
            new proxy('a');
            new proxy('b');
            baseLog(userList);      // [User {}, User{}]
        }
        {
            const userList = [];
            function emit(newValue) {
                baseLog(newValue);
            }
            const proxy = new Proxy(userList, {
                set(target, property, value, receiver) {
                    const result = Reflect.set(...arguments);
                    if (result) {
                        emit(Reflect.get(target, property, receiver));
                    }
                    return result;
                }
            });
            proxy.push('a');        // a
            proxy.push('b');        // b
        }
    }
}


///////////////////////////////////////////////////////////////////////////////////////
testLog();
testDelegateBase();
testCatcherAndReflection();
testProxyPattern();
